-
-
Notifications
You must be signed in to change notification settings - Fork 588
fix(android): correctly end view transitions #3250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix(android): correctly end view transitions #3250
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, this looks really sensible.
The views will remain visible as we marked them as "in transition". HOWEVER, the children will already get removed from the parents mChildren list. Only the children will keep their mParent field set
We call endRemovalTransition once the transition is done, trying to iterate over the children, but they are already unset. You can verify this by logging the children count when this method gets called
You're exactly right here. A while ago I fixed similar problem in core, yet here I've failed to notice this 😅
Therefore I'm all for landing part of this patch that applies to screens incorrectly ending view transition.
As for the view recycling part, I'll admit that we have not considered up to this moment much, as, correct me if I'm wrong, it is disabled by default on Android.
I would like to avoid (if possible) code for special handling of view recycling in screens. I'd just prefer to fix it in core.
So thanks to my PR to core, the ReactViewGroup
tracks which children have been removed during transition and which were not -> this is information we could inspect from child view to make sure, we've been removed from parent while being in transition, or we can add similar bit of logic to ReactViewGroupManager
(as also you suggest) - check whether the view has parent or not. This is the best method I've found back then.
[...] but it seems that there might be other code paths where the same native view is trying to be reused
Yeah, that can happen. In screens we've gone a while ago with "recycle method" that solved this for screen instances. Might be reasonable to also upstream it to core. But I'd be more in favour of identifying particular code paths & culprits that lead to those kind of crashes instead of "recycling" every view in core, as it would only encourage incorrect code from libraries side.
yes thats correct, also not sure if this is ever gonna land in stable 😅 It was just one way for me to reproduce this crash.
Ah yeah, thats nice, i will experiment with that! Okay cool, so then i'd remove everything else which i added for reproduction from this PR and we can move forward with the main change? 😊 |
Yes, we can. Let's get this PR up to date & I think we can proceed. |
Cool, there is just one thing that might needs to be changed. I had this change in production and saw today the following reports in sentry:
I can see from the breadcrumbs, that this crash happens, while we are still executing My fix for this right now would be to always run // Note: this can be called from the JS thread 👀
fun startRemovalTransition() {
// Dispatch on the main thread if not already on it
val mainLooper = Looper.getMainLooper()
if (Looper.myLooper() != mainLooper) {
Handler(mainLooper).post {
startRemovalTransition()
}
return
}
if (isBeingRemoved) return
isBeingRemoved = true
startTransitionRecursive(this)
} |
I see. |
We have this one here: #2964 I'll see to that one first & see whether we can land it before we land this one. |
Description
Fixes #3249
Note
This PR still contains the reproduction code necessary + it contains changes related to RN core. This is meant so we can have a conversation about this issue and see how to apply the fixes best.
There are two problems which this PR tries to face to fix usage with recycling views:
1.
Screen#endRemovalTransition
is broken:react-native-screens/android/src/main/java/com/swmansion/rnscreens/Screen.kt
Lines 472 to 477 in 4982d4c
Here you can see that we try to iterate over the children of the screen to end the view transition. The problem is that the react-native mounting layer already has all the children removed from the parent. The flow of events is:
NativeProxy
startRemovalTransition
. All children will be marked as "in-transition"removeView
for all the children of our screenmChildren
list. Only the children will keep theirmParent
field setendRemovalTransition
once the transition is done, trying to iterate over the children, but they are already unset. You can verify this by logging the children count when this method gets calledendViewTransition(child)
and theirmParent
field never gets reset. Thus the react-native mounting layer will always crash when trying to reuse this view.2. Interrupting a screen transition can cause a wrong order of events
We rely on
Screen#endRemovalTransition
to be called beforeScreenStack#endRemovalTransition
.ScreenStack
is in some way the parent ofScreen
. So the parent of the screen first callsendViewTransition
. This is problematic because:endViewTransition
it will loop over all children and calldispatchDetachedFromWindow()
:mDisappearingChildren
Screen#endRemovalTransition
and loop over the views callingendViewTransition
will have no effect, since the views are no longer marked as disappearing:mParent
field never gets reset. Thus the react-native mounting layer will always crash when trying to reuse this view.Changes
1. Fixing
Screen#endRemovalTransition
is broken:I simply added a list of parent-child pairs that we add to in
startRemovalTransition
. Then whenendRemovalTransition
gets called we loop over that list.2. Fixing interrupting a screen transition can cause a wrong order of events:
Two fixes are necessary for this:
ScreenStack
we callScreen#endViewTransition
first, before calling intosuper.endViewTransition
. I think this is safe sinceScreen#endRemovalTransition
has a flag checking if we are currently in a removal transition or notendViewTransition
from the most inner children up to the top parent so we avoid the problem explained above, where callingendViewTransition
on a parent will remove all children frommDisappearingChildren
listAdditional changes in react-native core
As you can see I patched RN + override the default
ReactViewManager
. I think we also need to address this in react-native somehow. There are two things that need to be addressed I believe:onDropViewInstance
) we could check if a view still has a parent. If so we could assume its in transition (or maybe have it marked as such earlier), and only add the view to the recyling pool on detach (ie. when the transition has ended)addView
in theReactViewManager
/BaseViewManager
that will check if a view is "in-transition" / has a parent, and only attach the view once the transition is done (ie. on detach)➤ I think that we can still apply the changes in RNS without needing to wait for react-native core though.
Screenshots / GIFs
Screen_Recording_20250924_151216_FabricExample.mp4
Screen_Recording_20250925_102125_FabricExample.mp4
Test code and steps to reproduce
You can simply run this PR and go to the
TestRecylingViews
tab in the example app to play with it.Checklist